...loading
2025-06-21
프론트엔드는 사용자와 서버 사이의 첫 점점이다. 그렇기에 사용자를 보호하고 서비스의 신뢰를 유지하는 것 또한 매우 중요하다.
하지만 프론트엔드는 사용자에게 직접 보여지는 화면을 구성하는 영역이기 때문에, 종종 보안보다는 UI와 기능 구현에 집중하게 되는 상황이 발생하곤 한다. 주니어 개발자인 필자 역시 보안의 중요성과 구체적인 실천 방안을 명확히 인지하지 못한 채 개발하는 순간들이 있어왔다.
따라서 이번 포스팅을 통해 프론트엔드 개발에서 보안이 왜 중요한지 구체적으로 살펴보고,
프론트엔드 개발자로서 실천할 수 있는 보안 수칙들을 정리하고자 한다.
프론트엔드에서 보안이 중요한 구체적인 이유는 다음과 같다.
이러한 사항들이 무너질 경우, 사용자와 서비스 모두에게 큰 리스크가 발생한다. 따라서 구체적인 위험요소들을 인지하고 이에 대해 주의하며 서비스를 개발하고 운영해나가야 한다.
XSS(Cross-Site Scripting)는 공격자가 웹페이지에 악성 스크립트(javascript)를 삽입해 다른 사용자의 브라우저에서 실행되게 만드는 공격이다. 이 공격을 통해 공격자는 사용자의 쿠키 탈취, 세션 탈취, 브라우저 제어, 가짜 로그인 창 띄우기 등의 다양한 해킹을 시도할 수 있다.
<scirpt>alert('해킹!')</script>
간단한 예시로, 공격자가 서비스의 댓글 창에 위와 같은 스크립트 코드를 입력해 제출했다고 가정하자. 그렇다면 해당 댓글 확인하는 다른 사용자들의 브라우저에서 공격자의 스크립트가 실행될 위험이 있다.
리액트에서는 JSX를 사용할 때 자동으로 HTML escape 처리를 해준다. 따라서 공격자가 입력창에 위와 같은 스크립트 코드를 삽입한다 하더라도 자동으로 문자열을 변환시켜 스크립트코드가 실행되지 않는다.
<script>alert('해킹!')</script> // 자동으로 '<', '>' 변환
그러므로 리액트를 사용하는 것 자체만으로, XSS 위험을 방지할 수 있게 된다. 하지만 개발을 하다보면 HTML 문자열 그대로를 DOM에 삽입해야하는 상황이 있다. 이러한 경우에 리액트의 dangerouslySetInnerHTML
API를 사용한다.
dangerousluSetInnerHTML
API는 HTML 문자열을 그대로 DOM에 삽입할 수 있도록 한다. 해당 API를 사용한 JSX 객체는 html 스크립트를 escape처리를 무시하게 된다. 즉, 본래의 XSS 보호 장치를 무력화하는 것이다. 따라서 dangerousluSetInnerHTML
는 왠만해서는 사용하지 않는 것이 좋다.
하지만 필요한 경우들이 있다. 대표적으로 사용자/관리자가 작성한 마크다운을 html로 보여줘야 할 때다. 이럴 때는 DOMpurify
, xss-filter
, sanitize-html
등의 라이브러리를 통해 'script', 'iframe' 등의 위험요소가 있는 태그들을 정제(sanitization) 해야 한다.
나아가 스크립트 형태로 제공되는 외부 서비스들을 사용하는 경우가 있다. 이 상황에서 정제 라이브러리를 사용한다면, 'script'와 같은 태그가 정제되어 제대로 서비스가 제대로 작동되지 않는다. 따라서 어쩔 수 없이 dangerouslyInnerHTML
을 사용해야한다고 생각할 수 있다.
하지만 이럴 경우, useEffect
를 사용하여 JS로 직접 DOM을 조작하면 보다 안전하게 기능을 개발 할 수 있다. 이러한 방식을 통해 '스크립트 DOM을 삽입한다'라는 코드의 명확성이 증가하고, JSX로 렌더링 되는 태그들의 escape처리도 보장하여 코드의 안정성이 증가한다.
// 👍 Good import { useEffect } from 'react'; export default function ThirdPartyWidget() { // 스크립트를 삽입하는 로직임이 분명하여, 추후 유지보수 시에도 보안 강화에 용이 ( 악성 스크립트일 경우 빠르게 분별 ) useEffect(() => { const script = document.createElement('script'); script.src = 'https://widget-provider.com/widget.js'; script.async = true; document.body.appendChild(script); }, []); return <div id="widget-container" />; } // ❌ Bad export default function ThirdPartyWidget() { // JSX 코드에 가려져 '스크립트 삽입의 명확성'이 떨어짐 + XSS 방어선 무력화 return <div id="widget-container" dangerouslySetInnerHTML={{ __html: '<script src="..."></script>' }} />; }
CSRF(Cross Site Request Forgery)란 사이트 간 요청 위조를 의미한다. 사용자가 인증된 세션을 보유한 상태에서, 공격자가 세션을 이용해 의도치 않은 요청을 몰래 보내는 것이다.
이처럼 CSRF는 공격자가 직접 인증 정보를 탈취하는 것이 아니라, 브라우저에 이미 저장된 세션 쿠키를 자동으로 활용하여 공격을 수행한다
프론트엔드는 CSRF를 직접적으로 막을 수는 없지만, 백엔드와의 충분한 논의와 주의사항을 검토함으로써 CSRF 방어에 보조적인 역할을 수행할 수 있다. 프론트엔드가 주의해야할 사항들은 다음과 같다.
1. credentials : include
남용 금지
credentails
는 브라우저가 요청을 보낼 때 쿠키나 인증 정보를 포함할지 결정하는 옵션이다. 해당 옵션에는 아래와 같은 속성값들이 있다.
만약 JWT 토큰을 발급받아 Local/Session Storage
에 저장하여 직접 헤더에 붙인다면 credentials
를 사용하지 않아도 된다.
값 | 설명 |
---|---|
omit | 기본값; 인증 정보를 전송하지 않음 |
same-origin | 동일 Origin(도메인)에만 인증 정보를 자동으로 전송 |
include | 교차 Origin 요청이라도 인증 정보를 전송 (주의 필요) |
해당 속성들 중에서 include
는 교차 Origin 요청이더라도 쿠키를 포함하기 때문에, 무분별하게 사용한다면 CSRF 공격의 표적이 될 수 있다. 따라서 쿠키 기반 인증을 사용하는 요청에만 제한적으로 사용하는 것이 중요하다.
2. 서버로부터 내려받은 CSRF 토큰을 반드시 요청에 포함
CSRF토큰은 임의의 난수 값(토큰)으로, CSRF 공격을 구분하기 위해 사용된다. 사용자가 중요한 요청을 보낼 때, 서버로부터 전달받은 CSRF 토큰을 헤더에 직접 담아 보낸다. 요청을 전달받은 서버는 쿠키의 인증 정보와 요청에 포함된 CSRF 토큰을 모두 확인하여 요청을 정상처리한다. 결과적으로 악성 사이트는 이 CSRF 토큰의 값을 알 수 없기 때문에 공격을 차단할 수 있다.
3. CORS 정책을 꼼꼼히 검토
프론트와 백엔드가 분리되어 있는 구조라면, 백엔드와 충분한 논의와 검토를 통해 특정 Origin만 허용하는지 확인한다.
Comments